﻿using SIR.Common.Condition;
using SIR.DAL;
using SIR.Entities.Implementations;
using System.Linq;
using System.Net.Mail;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Http;

namespace SIR.Web.API
{
	/// <summary>
	/// WebAPI controller handling the authentication
	/// </summary>
	public class AuthenticationController : ApiController
	{
		/// <summary>
		/// Regular expression for validating passowrds
		/// </summary>
		private static readonly Regex _passwordValidation = new Regex(@"(?=.{8,})(?=(.*\d){1,})(?=(.*\W){1,})");

		/// <summary>
		/// Logs in a session based on the passed credentials
		/// </summary>
		/// <param name="cred">The authentication credentials</param>
		/// <returns>The response and user if authentication was successful</returns>
		[System.Web.Http.HttpPost]
		[System.Web.Http.ActionName("LogIn")]
		public Response<User> LogIn(Credentials cred)
		{
			User user = null;

			// When no credentials supplied
			if (cred == null)
			{
				// Try to log in by the session
				long? userId = null;
				SessionManager.GetSession(HttpContext.Current, out userId, false);
				// Return empty fail response
				if (!userId.HasValue)
					return Response<User>.CreateResponse(false, null); // No response, result is expected
				// Was able to log in by session, find the user object
				user = DAL.DataServer.Find<User>(new Predicate("Id", userId.Value, PredicateOperator.EqualTo)).Results.FirstOrDefault();
			}
			else
			{
				// Email and password must be specified
				if (string.IsNullOrWhiteSpace(cred.email))
					return Response<User>.CreateResponse(false, null, "Email address is required to log in");
				if (string.IsNullOrWhiteSpace(cred.password))
					return Response<User>.CreateResponse(false, null, "Password is required to log in");
				// Find the user by their credentials
				user = FindUser(cred).Results.FirstOrDefault();
				// Apply the found user (or no found user) to the session
				SessionManager.SetSession(HttpContext.Current, user);
			}

			// Create the response with the user and appropriate message
			var response = Response<User>.CreateResponse(user != null, user,
				user == null ? "Email and/or password is invalid" : "Authentication Successful");

			if (user == null)
			{
				// Try to find out if password was wrong
				user = FindUser(cred, false).Results.FirstOrDefault();

				if (user != null)
				{
					// Email valid, password invalid, Increment lockout
					user.InvalidAttempts++;
					var results = DataServer.Save<User>(user);
				}
			}
			else if (user.InvalidAttempts != 0) // Log in successful, reset failed attempts
			{
				// Reset lockout
				user.InvalidAttempts = 0;
				DataServer.Save<User>(user);
			}
			// Return the response
			return response;
		}

		/// <summary>
		/// Finds a user based on the credentials
		/// </summary>
		/// <param name="cred">The creditials to use to find the user</param>
		/// <param name="checkPassword">Whether or not to find the user using authentication logic or just email</param>
		/// <returns>The DataServerResults containin the found user or not</returns>
		private static DataServerResults<User> FindUser(Credentials cred, bool checkPassword = true)
		{
			IPredicate condition = null;
			if (checkPassword)
			{
				// Ensure email and password match with invalid attempts less than 3
				condition = new PredicateGroup(new[] {
					new Predicate("Email", cred.email, PredicateOperator.EqualTo),
					new Predicate("Password", cred.password, PredicateOperator.EqualTo),
					new Predicate("InvalidAttempts", 3, PredicateOperator.LessThan)
				}, true);
			}
			else
			{
				// Find by email only
				condition = new Predicate("Email", cred.email, PredicateOperator.EqualTo);
			}
			// Return the results
			return DataServer.Find<User>(condition);
		}

		/// <summary>
		/// Registers a new user
		/// </summary>
		/// <param name="cred">The new user's credentials</param>
		/// <returns>The response containing any message and/or the newly created user</returns>
		[System.Web.Http.HttpPost]
		[System.Web.Http.ActionName("Register")]
		public Response<User> Register(NewUser cred)
		{
			// Verify email and password are supplied
			if (string.IsNullOrWhiteSpace(cred.email))
				return Response<User>.CreateResponse(false, null, "Email address is required to register a new user");
			if (string.IsNullOrWhiteSpace(cred.password))
				return Response<User>.CreateResponse(false, null, "Password is required to register a new user");

			// Check to see if the user already exists
			var userDataResults = FindUser(cred, false);

			if (userDataResults.WasSuccessful)
			{
				// User found?
				var user = userDataResults.Results.FirstOrDefault();
				if (user != null)
				{
					// The user already exists
					return Response<User>.CreateResponse(false, null, "The email address has already been associated to another account");
				}

				// Check Password
				if (!_passwordValidation.IsMatch(cred.password))
					return Response<User>.CreateResponse(false, null, "Must be at least 8 characters long\nMust contain at least one digit\nMust contain at least one special (non-alphanumeric) character");
				// Save the new user
				var saveResults = DataServer.Save<User>(new User() { Nickname = cred.nickname, Email = cred.email, Password = cred.password });

				if (saveResults.WasSuccessful)
				{
					// Get the new user
					user = saveResults.NewValue;

					// Create Folder For Library
					var folder = DataServer.Save<Folder>(new Folder() { UserId = user.Id, Name = "Library" }).NewValue;
					// Create Library
					var response = DataServer.Save<Library>(new Library(folder) { });
					// Set the user's session
					SessionManager.SetSession(HttpContext.Current, user);
					// Send registration email
					SendEmail(user.Email, "SIR - Welcome", "Thank you for for registering with SIR. You can now upload your pictures to your library and mark other\'s pictures as your favorites.");
					// Send successful response
					return Response<User>.CreateResponse(true, user, "Registration Successful");
				}
			}

			// An error occured, return a failed response
			return Response<User>.CreateResponse(false, null, "Could not save the new user");
		}

		/// <summary>
		/// Logs the currently authenticated user out of the system
		/// </summary>
		/// <returns>True if the log out was successful</returns>
		[System.Web.Http.HttpPost]
		[System.Web.Http.ActionName("LogOut")]
		public bool LogOut()
		{
			long? userId;
			// Find the current user by the session
			SessionManager.GetSession(HttpContext.Current, out userId, false);
			// No authenticated user
			if (!userId.HasValue)
				return true;
			// Find the session object
			var sessionResults = DataServer.Find<SirSession>(new Predicate("UserId", userId.Value, PredicateOperator.EqualTo));
			// Delete all session objects for the user
			foreach (var session in sessionResults.Results)
			{
				session.IsDeleted = true;
				DataServer.Save<SirSession>(session);
			}
			// Successful log out
			return true;
		}

		/// <summary>
		/// Changes the password for the authenticated user
		/// </summary>
		/// <param name="passwordChange">The old and new password for the user</param>
		/// <returns>A response containing the user and any messages</returns>
		[System.Web.Http.HttpPost]
		[System.Web.Http.ActionName("ChangePassword")]
		public Response<User> ChangePassword(PasswordChange passwordChange)
		{
			long? userId;
			// Find the user by session
			SessionManager.GetSession(HttpContext.Current, out userId, false);
			// There is no authenticated user
			if (!userId.HasValue)
				return Response<User>.CreateResponse(false, null, "You must be authenticated before changing your password");
			// Validate the password
			var user = DAL.DataServer.Find<User>(new PredicateGroup(new[] {
				new Predicate("Id", userId.Value, PredicateOperator.EqualTo),
				new Predicate("Password", passwordChange.oldPassword, PredicateOperator.EqualTo)
				},
				true)).Results.FirstOrDefault();

			// Could not authenticate with the old password
			if (user == null)
				return Response<User>.CreateResponse(false, null, "Current password was invalid");

			// Check new Password
			if (!_passwordValidation.IsMatch(passwordChange.newPassword))
				return Response<User>.CreateResponse(false, null, "Must be at least 8 characters long\nMust contain at least one digit\nMust contain at least one special (non-alphanumeric) character");
			// Set the new password and save
			user.Password = passwordChange.newPassword;
			var result = DataServer.Save<User>(user);
			// Send the response back
			return Response<User>.CreateResponse(result.WasSuccessful, user,
				result.WasSuccessful ? "Password changed successfully" : result.ErrorMessage);
		}
		/// <summary>
		/// Resets the password for the specified user
		/// </summary>
		/// <param name="cred">The credentials for the user, only email is required</param>
		/// <returns>The response containg the message</returns>
		[System.Web.Http.HttpPost]
		[System.Web.Http.ActionName("ResetPassword")]
		public Response<User> ResetPassword(Credentials cred)
		{
			// Email is required
			if (cred == null || string.IsNullOrWhiteSpace(cred.email))
				return Response<User>.CreateResponse(false, null, "Email is required to reset the password");
			// Find the user by email
			var user = FindUser(new Credentials() { email = cred.email, password = string.Empty }, false).Results.FirstOrDefault();
			// If user was found
			if (user != null)
			{
				// Generate random password and save it
				user.Password = PasswordGenerate.GeneratePassword(8, 10);
				DAL.DataServer.Save<User>(user);
				// Send the password reset email
				SendEmail(user.Email, "Password Reset", "Your password has been reset to: " + user.Password);
			}
			// Do not send the user object back, creates a security hole
			return Response<User>.CreateResponse(true, null, "Password reset successfully");
		}
		/// <summary>
		/// Send an email to a user
		/// </summary>
		/// <param name="to">Email address to send the email to</param>
		/// <param name="subject">Subject of the email</param>
		/// <param name="body">Body of the email</param>
		private static void SendEmail(string to, string subject, string body)
		{
			// Pull SMTP settings from the web.config
			var client = new SmtpClient();
			var message = new MailMessage();
			message.To.Add(to);
			message.Subject = subject;
			message.Body = body;
			// Send the email
			client.Send(message);
		}
	}

	/// <summary>
	/// Authentication credentials
	/// </summary>
	public class Credentials
	{
		/// <summary>
		/// Gets or sets the email
		/// </summary>
		public string email { get; set; }
		/// <summary>
		/// Gets or sets the password
		/// </summary>
		public string password { get; set; }
	}

	/// <summary>
	/// New User credentials
	/// </summary>
	public class NewUser : Credentials
	{
		/// <summary>
		/// Gets or sets the nickname
		/// </summary>
		public string nickname { get; set; }
	}
	/// <summary>
	/// Password change criteria
	/// </summary>
	public class PasswordChange
	{
		/// <summary>
		/// Gets or sets the current password for the user
		/// </summary>
		public string oldPassword { get; set; }
		/// <summary>
		/// Gets or sets the new password for the user
		/// </summary>
		public string newPassword { get; set; }
	}
}
